1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.filesystem.hipfs; 12 13 public import hip.api.filesystem.hipfs; 14 import hip.util.reflection; 15 16 /** 17 * Returns whether if the path attempts to exit the initial one. 18 * Params: 19 * initial = 20 * toAppend = 21 * Returns: 22 */ 23 private pure bool validatePath(string initial, string toAppend) 24 { 25 import hip.util.array:lastIndexOf; 26 import hip.util.string:splitRange; 27 import hip.util.system : sanitizePath; 28 29 if(initial.length != 0 && initial[$-1] == '/') 30 initial = initial[0..$-1]; 31 scope char[] newPath = initial.sanitizePath; 32 scope char[] appends = toAppend.sanitizePath; 33 34 scope(exit) 35 { 36 import core.memory:GC; 37 // GC.free(newPath.ptr); 38 // GC.free(appends.ptr); 39 } 40 41 foreach(a; splitRange(appends, "/")) 42 { 43 if(a == "" || a == ".") 44 continue; 45 if(a == "..") 46 { 47 long lastInd = newPath.lastIndexOf('/'); 48 if(lastInd == -1) 49 continue; 50 newPath = newPath[0..cast(uint)lastInd]; 51 } 52 else 53 newPath~= "/"~a; 54 } 55 for(int i = 0; i < initial.length; i++) 56 if(initial[i] != newPath[i]) 57 return false; 58 return true; 59 } 60 61 ///Function is implemented AppDelegate.m 62 version(AppleOS) 63 private extern(C) const(char*) hipGetResourcesPath(); 64 65 abstract class HipFile : IHipFileItf 66 { 67 immutable FileMode mode; 68 immutable string path; 69 ulong size; 70 ulong cursor; 71 @disable this(); 72 this(string path, FileMode mode) 73 { 74 this.mode = mode; 75 this.path = path; 76 open(path, mode); 77 this.size = getSize(); 78 } 79 ///Whence is the same from libc 80 long seek(long count, int whence = SEEK_CUR) 81 { 82 switch(whence) 83 { 84 default: 85 case SEEK_CUR: 86 cursor+= count; 87 break; 88 case SEEK_END: 89 cursor = size + count; 90 break; 91 case SEEK_SET: 92 cursor = count; 93 break; 94 } 95 return cast(long)cursor; 96 } 97 98 T[] rawRead(T)(T[] buffer) 99 { 100 read(cast(void*)buffer.ptr,buffer.length); 101 return buffer; 102 } 103 } 104 105 106 private class HipFSPromise : IHipFSPromise 107 { 108 string filename; 109 bool finished = false; 110 ubyte[] data; 111 void delegate(in ubyte[] data)[] onSuccessList; 112 void delegate(string err)[] onErrorList; 113 this(string filename){this.filename = filename;} 114 IHipFSPromise addOnSuccess(void delegate(in ubyte[] data) onSuccess) 115 { 116 if(finished) 117 onSuccess(data); 118 else 119 onSuccessList~=onSuccess; 120 return this; 121 } 122 IHipFSPromise addOnError(void delegate(string error) onError) 123 { 124 if(finished && !data.length) 125 onError("No data"); 126 else 127 onErrorList~= onError; 128 return this; 129 } 130 void setFinished(ubyte[] data) 131 { 132 import std.stdio; 133 this.data = data; 134 this.finished = true; 135 if(data) foreach(success; onSuccessList) 136 success(data); 137 else foreach(err; onErrorList) 138 err("Could not read file"); 139 140 } 141 bool resolved() const{return finished;} 142 } 143 144 /** 145 * FileSystem access for specific platforms. 146 */ 147 class HipFileSystem 148 { 149 protected __gshared string defPath; 150 protected __gshared string initialPath = ""; 151 protected __gshared string combinedPath; 152 protected __gshared bool isInstalled; 153 protected __gshared IHipFileSystemInteraction fs; 154 protected __gshared size_t filesReadingCount = 0; 155 156 protected __gshared bool function(string path, out string errMessage)[] extraValidations; 157 158 version(Android){import hip.filesystem.systems.android;} 159 else version(UWP){import hip.filesystem.systems.uwp;} 160 else version(WebAssembly){import hip.filesystem.systems.browser;} 161 else version(PSVita){import hip.filesystem.systems.cstd;} 162 else version(CustomRuntimeTest){import hip.filesystem.systems.cstd;} 163 else version(HipDStdFile){import hip.filesystem.systems.dstd;} 164 else {import hip.filesystem.systems.cstd;} 165 166 public static void initializeAbsolute() 167 { 168 if(fs is null) 169 { 170 version(Android){fs = new HipAndroidFileSystemInteraction();} 171 else version(UWP){fs = new HipUWPileSystemInteraction();} 172 else version(PSVita){fs = new HipCStdioFileSystemInteraction();} 173 else version(CustomRuntimeTest){fs = new HipCStdioFileSystemInteraction();} 174 else version(WebAssembly){fs = new HipBrowserFileSystemInteraction();} 175 else 176 { 177 version(HipDStdFile){}else{static assert(false, "HipDStdFile should be marked to be used.");} 178 fs = new HipStdFileSystemInteraction(); 179 } 180 } 181 } 182 183 184 public static void install(string path) 185 { 186 import hip.util.system : sanitizePath; 187 if(!isInstalled) 188 { 189 initialPath = path.sanitizePath; 190 setPath(""); 191 isInstalled = true; 192 } 193 } 194 /** 195 * This function may be refactored in future since having different 196 * directories to resources to writeable paths is becoming more common 197 */ 198 version(AppleOS) 199 public static string getResourcesPath() 200 { 201 import core.stdc.string; 202 auto str = hipGetResourcesPath; 203 return cast(string)str[0..strlen(str)]; 204 } 205 206 207 public static void install(string path, 208 bool function(string path, out string errMessage)[] validations ...) 209 { 210 import hip.util.system : sanitizePath; 211 if(!isInstalled) 212 { 213 install(path); 214 foreach (v; validations){extraValidations~=v;} 215 } 216 } 217 @ExportD public static string getPath(string path) 218 { 219 import hip.util.path:joinPath; 220 import hip.util.system : sanitizePath; 221 import hip.console.log; 222 223 if(combinedPath) 224 return joinPath(combinedPath, path.sanitizePath); 225 return path.sanitizePath; 226 } 227 @ExportD public static bool isPathValidExtra(string path) 228 { 229 import hip.error.handler; 230 import hip.util.system : sanitizePath; 231 path = path.sanitizePath; 232 string err; 233 foreach (bool function(string, out string) validation; extraValidations) 234 { 235 if(!validation(path, err)) 236 { 237 ErrorHandler.showErrorMessage("HipFileSystem validation error", 238 "Path '"~path~"' failed at validation with error: '"~err~"'."); 239 return false; 240 } 241 } 242 return true; 243 } 244 245 @ExportD public static bool isPathValid(string path, bool expectsFile = true, bool shouldVerify = true) 246 { 247 import hip.error.handler; 248 if(!initialPath) return false; 249 if(!validatePath(initialPath, defPath~path)) 250 { 251 ErrorHandler.showErrorMessage("Path failed default validation: can't reference external path.", path); 252 return false; 253 } 254 if(shouldVerify) 255 { 256 if((expectsFile && !HipFS.absoluteIsFile(path)) || (!expectsFile && !HipFS.absoluteIsDir(path))) 257 { 258 ErrorHandler.showErrorMessage("Path failed default validation: Expected '"~ (expectsFile ? "file" : "directory") ~ 259 "' but received "~ (expectsFile ? "'directory'" : "'file'"), path); 260 return false; 261 } 262 } 263 264 return isPathValidExtra(path); 265 } 266 267 @ExportD public static bool setPath(string path) 268 { 269 import hip.util.path:joinPath; 270 import hip.util.system : sanitizePath; 271 import hip.console.log; 272 if(path) 273 { 274 defPath = path.sanitizePath; 275 combinedPath = joinPath(initialPath, defPath); 276 } 277 else 278 combinedPath = initialPath; 279 return validatePath(initialPath, combinedPath); 280 } 281 282 private static void defaultErrorHandler(string err = "") 283 { 284 import hip.error.handler; 285 filesReadingCount--; 286 ErrorHandler.assertExit(false, "HipFS Error: "~err); 287 } 288 289 @ExportD public static IHipFSPromise read(string path) 290 { 291 import hip.console.log; 292 hiplog("Required path ", path); 293 path = getPath(path); 294 if(!isPathValid(path)) 295 return null; 296 hiplog("Path validated."); 297 filesReadingCount++; 298 299 HipFSPromise promise = new HipFSPromise(path); 300 fs.read(path, (ubyte[] data) 301 { 302 filesReadingCount--; 303 promise.setFinished(data); 304 }, (string err) 305 { 306 promise.setFinished(null); 307 defaultErrorHandler(err); 308 }); 309 310 return promise; 311 } 312 313 @ExportD public static IHipFSPromise readText(string path) 314 { 315 IHipFSPromise ret = read(path); 316 // if(ret) 317 // { 318 // import std.utf; 319 // output = toUTF8((cast(string)data)); 320 // } 321 return ret; 322 } 323 324 325 version(HipDStdFile) 326 { 327 import std.stdio:File; 328 public static bool getFile(string path, string opts, out File file) 329 { 330 if(!isPathValid(path)) 331 return false; 332 file = File(getPath(path), opts); 333 return true; 334 } 335 336 } 337 338 @ExportD public static bool write(string path, void[] data) 339 { 340 if(!isPathValid(path)) 341 return false; 342 return fs.write(getPath(path), data); 343 } 344 @ExportD public static bool exists(string path){return isPathValid(path) && fs.exists(getPath(path));} 345 @ExportD public static bool remove(string path) 346 { 347 if(!isPathValid(path)) 348 return false; 349 return fs.remove(getPath(path)); 350 } 351 352 @ExportD public static string getcwd() 353 { 354 return getPath(""); 355 } 356 357 @ExportD public static bool absoluteExists(string path){return fs.exists(path);} 358 @ExportD public static bool absoluteIsDir(string path){return fs.isDir(path);} 359 @ExportD public static bool absoluteIsFile(string path){return fs.isFile(path);} 360 @ExportD public static bool absoluteRemove(string path){return fs.remove(path);} 361 @ExportD public static bool absoluteRead(string path, out void[] output) 362 { 363 ///This may need to be refactored in the future. 364 // import std.functional:toDelegate; 365 return fs.read(path, (void[] data){output = data;}, (err) => defaultErrorHandler(err)); 366 } 367 @ExportD("ubyte") public static bool absoluteRead(string path, out ubyte[] output) 368 { 369 void[] data; 370 bool ret = absoluteRead(path, data); 371 output = cast(ubyte[])data; 372 return ret; 373 } 374 375 @ExportD public static bool absoluteReadText(string path, out string output) 376 { 377 void[] data; 378 bool ret = absoluteRead(path, data); 379 if(ret) 380 output = cast(string)data; 381 return ret; 382 } 383 384 385 @ExportD public static bool isDir(string path){return isPathValid(path, false, false) && fs.isDir(getPath(path));} 386 @ExportD public static bool isFile(string path){return isPathValid(path, true, false) && fs.isFile(getPath(path));} 387 388 @ExportD public static string writeCache(string cacheName, void[] data) 389 { 390 import hip.util.path:joinPath; 391 string p = joinPath(initialPath, ".cache", cacheName); 392 write(p, data); 393 return p; 394 } 395 } 396 397 alias HipFS = HipFileSystem;